package apple.core;

import org.apache.log4j.Logger;

import apple.ui.IMonitor;

public class VideoGenerator {

   static Logger logger = Logger.getLogger(VideoGenerator.class);

   /*
    * To test: color=3, plot 10,10, color=15, plot 15,15 Why: Missing font for
    * character: p (112)
    */

   /*
    * There are two unrelated synchronized activities:
    * 1. on the suspend flag
    * 2. on the monitor
    * These two activities should not synchronize on the same object (current this) but 
    * rather on their own objects in order to reduce unnecessary possible contention.
    */
   
   private static final int SCAN_INTERVAL = 100;

   private static final int NUM_TEXT_LINES = 24;

   private static final int NUM_TEXT_COLUMNS = 40;

   private static final short TEXT_LORES_PAGE_1_BASE = 0x0400;

   private static final short TEXT_LORES_PAGE_2_BASE = 0x0800;

   private static final byte FOREGROUND_TEXT_COLOR_INDEX = 0xF; // WHITE

   private static final byte BACKGROUND_TEXT_COLOR_INDEX = 0x0; // BLACK

   public enum SoftSwitch {
      ALTCHAR, _80COL, _80STORE, PAGE2, TEXT, MIXED, HIRES, IOUDIS, DHIRES
   }

   /*
    * Begin soft switches. See page 29.
    */

   private boolean ALTCHAR = false;

   private boolean _80COL = false;

   private boolean _80STORE = false;

   private boolean PAGE2 = false;

   private boolean TEXT = true;

   private boolean MIXED = false;

   private boolean HIRES = false;

   private boolean IOUDIS = true;

   private boolean DHIRES = false;

   /*
    * End soft switches
    */

   public enum Format {
      NORMAL, INVERSE, FLASHING
   };

   private Memory memory;

   private IMonitor monitor;

   private boolean halt = false;

   private boolean suspend = false;

   /**
    * Sets the output monitor to which to render the screen images.
    * 
    * @param monitor the monitor or null to "unplug" the monitor
    */
   public synchronized void setMonitor(IMonitor monitor) {
      this.monitor = monitor;
   }

   public void setMemory(Memory memory) {
      this.memory = memory;
   }

   /*
    * Note: the values in the case statements come from the enum type, whereas
    * the values assigned come from the corresponding boolean values. Although
    * they share the same names, they are in different namespaces.
    */
   public byte readSoftSwitch(SoftSwitch softSwitch) {
      boolean bit7 = false;
      switch (softSwitch) {
      case ALTCHAR:
         bit7 = ALTCHAR;
         break;
      case _80COL:
         bit7 = _80COL;
         break;
      case _80STORE:
         bit7 = _80STORE;
         break;
      case PAGE2:
         bit7 = PAGE2;
         break;
      case TEXT:
         bit7 = TEXT;
         break;
      case MIXED:
         bit7 = MIXED;
         break;
      case HIRES:
         bit7 = HIRES;
         break;
      case IOUDIS:
         bit7 = IOUDIS;
         break;
      case DHIRES:
         bit7 = DHIRES;
         break;
      default:
         assert false : "unknown soft switch";
         break;
      }
      return getSoftSwitchByte(bit7);
   }

   public byte setSoftSwitch(SoftSwitch softSwitch, boolean state) {
      switch (softSwitch) {
      case ALTCHAR:
         ALTCHAR = state;
         break;
      case _80COL:
         _80COL = state;
         break;
      case _80STORE:
         _80STORE = state;
         break;
      case PAGE2:
         PAGE2 = state;
         break;
      case TEXT:
         TEXT = state;
         break;
      case MIXED:
         MIXED = state;
         break;
      case HIRES:
         HIRES = state;
         break;
      case IOUDIS:
         IOUDIS = state;
         break;
      case DHIRES:
         DHIRES = state;
         break;
      default:
         assert false : "unknown soft switch";
         break;
      }
      return getSoftSwitchByte(state);
   }

   /*
    * Bits 0-6 in bytes returned by reading a soft switch are undefined. In this
    * implementation, they are set to 0, although programs must not depend on
    * this. See page 30.
    */
   private byte getSoftSwitchByte(boolean bit7) {
      return bit7 ? (byte) 0x80 : 0x0;
   }

   /**
    * Signals the video generator to halt. This method does not block.
    */
   public synchronized void halt() {
      if (suspend) {
         resume();
      }
      halt = true;
   }

   public synchronized void suspend() {
      suspend = true;
   }

   public synchronized void resume() {
      suspend = false;
      this.notify();
   }

   public void run() {
      int imageData[][];

      while (!halt) {

         if (suspend) {
            synchronized (this) {
               while (suspend) {
                  try {
                     this.wait();
                  } catch (InterruptedException e) {
                     logger.debug(e);
                  }
               }
            }
         }

         if (TEXT) {
            imageData = generateTextImage();
         } else if (MIXED) {
            if (HIRES) {
               imageData = generateMixedHiResImage();
            } else {
               imageData = generateMixedLowResImage();
            }
         } else {
            if (HIRES) {
               imageData = generateHiResImage();
            } else {
               imageData = generateLowResImage();
            }
         }

         synchronized (this) {
            if (monitor != null) {
               monitor.drawImage(imageData);
            }
         }

         try {
            Thread.sleep(SCAN_INTERVAL);
         } catch (InterruptedException e) {
            // not a problem if awakened early
            logger.debug(e);
         }
      }
      // maybe one final draw of a blank screen?
   }

   /*
    * Any characters in FLASHING mode will be displayed as either NORMAL or
    * INVERSE in any given screen refresh. This algorithm determines which phase
    * for the current refresh.
    */
   private Format getFlashingPhase() {
      Format format;
      long millis = System.currentTimeMillis();
      int mod = (int) (millis & 0x3FF);
      if (mod < 0x100) {
         format = Format.NORMAL;
      } else if (mod < 0x200) {
         format = Format.INVERSE;
      } else if (mod < 0x300) {
         format = Format.NORMAL;
      } else {
         format = Format.INVERSE;
      }
      return format;
   }

   private int[][] generateTextImage() {

      int numRowElements = NUM_TEXT_LINES * Font.VERTICAL_PIXELS_PER_CHARACTER;
      int numColumnElements = NUM_TEXT_COLUMNS
            * Font.HORIZONTAL_PIXELS_PER_CHARACTER;
      int imageData[][] = new int[numRowElements][numColumnElements];

      short base = PAGE2 ? TEXT_LORES_PAGE_2_BASE : TEXT_LORES_PAGE_1_BASE;

      Format flashPhase = getFlashingPhase();
      for (int lineNumber = 0; lineNumber < NUM_TEXT_LINES; lineNumber++) {
         getTextLine(imageData, lineNumber, base, flashPhase);
      }
      return imageData;
   }

   private int[][] generateLowResImage() {

      int imageData[][] = new int[192][280];

      short base = PAGE2 ? TEXT_LORES_PAGE_2_BASE : TEXT_LORES_PAGE_1_BASE;

      for (int lineNumber = 0; lineNumber < NUM_TEXT_LINES; lineNumber++) {
         getLowResLine(imageData, lineNumber, base);
      }
      return imageData;
   }

   private int[][] generateMixedLowResImage() {

      int imageData[][] = new int[192][280];

      short base = PAGE2 ? TEXT_LORES_PAGE_2_BASE : TEXT_LORES_PAGE_1_BASE;

      for (int lineNumber = 0; lineNumber < 20; lineNumber++) {
         getLowResLine(imageData, lineNumber, base);
      }
      Format flashPhase = getFlashingPhase();
      for (int lineNumber = 20; lineNumber < 24; lineNumber++) {
         getTextLine(imageData, lineNumber, base, flashPhase);
      }
      return imageData;
   }

   private int[][] generateMixedHiResImage() {
      int imageData[][] = new int[192][280];
      return imageData;
   }

   private int[][] generateHiResImage() {
      int imageData[][] = new int[192][280];
      return imageData;
   }

   /*
    * See page 177
    */
   private void getTextLine(int[][] imageData, int lineNumber, short base,
         Format flashPhase) {

      int i = lineNumber / 8;
      int j = lineNumber % 8;
      base = (short) (base + i * 40 + j * 0x80);
      for (int k = 0; k < 40; k++) {
         short address = (short) (base + k);
         byte datum = memory.read(address);
         int[] characterPixels = getCharacterPixels(datum, flashPhase);
         int x = k * Font.HORIZONTAL_PIXELS_PER_CHARACTER;
         int y = lineNumber * Font.VERTICAL_PIXELS_PER_CHARACTER;
         for (int a = 0; a < characterPixels.length; a++) {
            imageData[y + a / Font.HORIZONTAL_PIXELS_PER_CHARACTER][x + a
                  % Font.HORIZONTAL_PIXELS_PER_CHARACTER] = characterPixels[a];
         }
      }
   }

   /*
    * See pages 20 and 14.
    */
   private int[] getCharacterPixels(byte datum, Format flashPhase) {
      Format format = Format.NORMAL;

      int group = (datum & 0xFF) >>> 5;
      byte code = (byte) (datum & 0x1F);
      byte ascii = code;
      switch (group) {
      case 0x0:
         ascii = (byte) (code | 0x40);
         format = Format.INVERSE;
         break;
      case 0x1:
         ascii = (byte) (code | 0x20);
         format = Format.INVERSE;
         break;
      case 0x2:
         if (!ALTCHAR) {
            ascii = (byte) (code | 0x40);
            format = Format.FLASHING;
         } else {
            // this should be MouseText
            ascii = (byte) (code | 0x40);
            format = Format.INVERSE;
         }
         break;
      case 0x3:
         if (!ALTCHAR) {
            ascii = (byte) (code | 0x20);
            format = Format.FLASHING;
         } else {
            ascii = (byte) (code | 0x60);
            format = Format.INVERSE;
         }
         break;
      case 0x4:
         ascii = (byte) (code | 0x40);
         format = Format.NORMAL;
         break;
      case 0x5:
         ascii = (byte) (code | 0x20);
         format = Format.NORMAL;
         break;
      case 0x6:
         ascii = (byte) (code | 0x40);
         format = Format.NORMAL;
         break;
      case 0x7:
         ascii = (byte) (code | 0x60);
         format = Format.NORMAL;
         break;
      }
      int[] characterPixels = getCharacterPixels((char) ascii, format,
            flashPhase);
      return characterPixels;
   }

   private static int[] getCharacterPixels(char character, Format format,
         Format flashPhase) {
      // this really should be initialized with backgroundColorIndex
      int numberPixels = Font.HORIZONTAL_PIXELS_PER_CHARACTER
            * Font.VERTICAL_PIXELS_PER_CHARACTER;
      int[] characterData = new int[numberPixels];
      int[] data = Font.getCharacterPixels(character);
      if (data == null) {
         logger.fatal("Missing font for character: " + character + " ("
               + (int) character + ")");
         System.exit(-1);
      }
      int pixelColor = FOREGROUND_TEXT_COLOR_INDEX;
      switch (format) {
      case NORMAL:
         break;
      case INVERSE:
         for (int i = 0; i < characterData.length; i++) {
            characterData[i] = pixelColor;
         }
         pixelColor = BACKGROUND_TEXT_COLOR_INDEX;
         break;
      case FLASHING:
         if (flashPhase == Format.INVERSE) {
            for (int i = 0; i < characterData.length; i++) {
               characterData[i] = pixelColor;
            }
            pixelColor = BACKGROUND_TEXT_COLOR_INDEX;
         }
         break;
      }
      for (int i : data) {
         characterData[i] = pixelColor;
      }
      return characterData;
   }

   private void getLowResLine(int[][] imageData, int lineNumber, short base) {

      int i = lineNumber / 8;
      int j = lineNumber % 8;
      base = (short) (base + i * 40 + j * 0x80);
      for (int k = 0; k < 40; k++) {

         byte datum = memory.read((short) (base + k));

         int x = k * Font.HORIZONTAL_PIXELS_PER_CHARACTER;
         int y = (i * 8 + j) * Font.VERTICAL_PIXELS_PER_CHARACTER;

         byte colorKey = (byte) (datum & 0x0F);
         drawLowResBlock(imageData, colorKey, x, y);

         colorKey = (byte) ((datum & 0xF0) >>> 4);
         drawLowResBlock(imageData, colorKey, x, y + 4);
      }
   }

   private void drawLowResBlock(int[][] imageData, byte colorKey, int x, int y) {
      for (int a = 0; a < 7; a++) {
         for (int b = 0; b < 4; b++) {
            imageData[y + b][x + a] = colorKey;
         }
      }
   }
}
